Woodbury Optimizations#4
Conversation
aschneuw
commented
Apr 6, 2026
- Improves Woodbury Identity optimizations
- properly takes into account pivoting for all Cholesky Decomposition based operations
- updates scikit-sparse to the latest version 0.5.0 (had breaking API changes)
- Introduces Ztilde_t <- phi.rphi %*% Zt as a further optimization
| if (length(ind)) { ## extract columns corresponding to non-smooth r.e.s | ||
| Zt <- getME(ret$mer,"Zt")[ind,] ## extracting random effects model matrix | ||
| root.phi <- getME(ret$mer,"Lambdat")[,ind] ## and corresponding sqrt of cov matrix (phi) (was ind,ind - seems wrong) | ||
| root.phi <- getME(ret$mer,"Lambdat")[ind,ind] ## and corresponding sqrt of cov matrix (phi) (was ind,ind - seems wrong) |
There was a problem hiding this comment.
this was exactly my thinking :)
fabian-s
left a comment
There was a problem hiding this comment.
Review Summary
Thanks for the work on this, Arno — the getVb() refactoring and the Woodbury identity path are well-structured and the core linear algebra is correct. Numerical equivalence testing (7 scenarios: Gaussian/binomial/Poisson with various smooth types, random intercepts/slopes) confirms machine-precision agreement between the R-only path and master, and between the Woodbury and direct paths.
However, there are several issues that need to be addressed before this can be merged. See inline comments for details.
Must Fix
python_cholmodhardcoded toTRUE— will crash any user without Python/scikit-sparse- Permutation direction in
phi.rphi— likely usespermwhereinv_permis needed root.phipotentially undefined — when no non-smooth random effects exist
Should Fix
- Comment typo introduced ("--get need")
- Stale/misleading comment on
root.philine scikit-sparse==0.5.0exact pin is fragile- Dead
Vcomputation - Dead
if (TRUE) {} else {}branches - Indentation inconsistency
| if (length(ind)) { ## extract columns corresponding to non-smooth r.e.s | ||
| Zt <- getME(ret$mer,"Zt")[ind,] ## extracting random effects model matrix | ||
| root.phi <- getME(ret$mer,"Lambdat")[,ind] ## and corresponding sqrt of cov matrix (phi) (was ind,ind - seems wrong) | ||
| root.phi <- getME(ret$mer,"Lambdat")[ind,ind] ## and corresponding sqrt of cov matrix (phi) (was ind,ind - seems wrong) |
There was a problem hiding this comment.
Stale comment: (was ind,ind - seems wrong) — but the code IS using [ind,ind], same as master. The parenthetical should be removed to avoid confusion.
|
Sorry for the chaos - briefly lost control of my coding agent ... ;) |
a89eb80 to
b1f6854
Compare
|
I can’t push to I rechecked this locally with
The direct-path fix is to use Suggested code changes in if (woodbury && nrow(Zt)) {
if (python_cholmod) {
phi_py = reticulate::r_to_py(phi)$copy()
cho_phi_py <- try(
cholmod$cho_factor(phi_py, supernodal_mode="auto", lower=TRUE),
silent = TRUE
)
if (inherits(cho_phi_py, "try-error")) {
woodbury <- FALSE
} else {
perm <- as.integer(reticulate::py_to_r(cho_phi_py$get_perm()) + 1)
inv_perm <- perm
inv_perm[perm] <- 1:length(perm)
phi.rphi <- reticulate::py_to_r(scipy_sparse$csc_matrix(cho_phi_py$R))[,inv_perm]
}
} else {
phi.inv <- try(chol2inv(chol(phi)), silent = TRUE)
if (inherits(phi.inv,"try-error")) woodbury <- FALSE
}
}if (python_cholmod) {
# Only system="A" applies CHOLMOD's internal permutation correctly.
V_py = reticulate::r_to_py(V)$copy()
R <- cholmod$cho_factor(V_py, supernodal_mode="auto", lower=TRUE)
XFP_py <- reticulate::r_to_py(Xfp)$copy()
XF_py <- reticulate::r_to_py(Xf)$copy()
XVXS <- t(Xfp) %*% reticulate::py_to_r(scipy_sparse$csc_matrix(R$solve(XFP_py, system="A"))) + Sp^2/scale
XVX <- t(Xf) %*% reticulate::py_to_r(scipy_sparse$csc_matrix(R$solve(XF_py, system="A")))
} else {
R <- mgcv::mchol(V);piv <- attr(R,"pivot")
WX <- as(solve(t(R),Xfp[piv,]),"matrix")
XVX <- as(solve(t(R),Xf[piv,]),"matrix")
XVX <- crossprod(XVX)
XVXS <- crossprod(WX)+Sp^2/scale
}gam.terms <- attr(gmf,"terms") # terms object for `gam' part of fit -- need this for prediction to work properlyand remove the dead line: V <- Diagonal(n=length(v),x=v)I also recommend resetting the file modes on:
Locally I validated the patched version with:
From my side, once the above is applied, this looks ready to merge. |
86d3303 to
1754ea1
Compare
… and further optimizations
1754ea1 to
9574240
Compare
Applied your suggested changes. Removed "# Only system="A" applies CHOLMOD's internal permutation correctly." Since this was misleading to me. I think the previous approach would have been doable as well with the appropriate permutations applied. But your approach works as well and is better. |
fabian-s
left a comment
There was a problem hiding this comment.
- please resolve my earlier comment on line gamm4.r l. 587
- since the
python_cholmod = TRUEbranch needspy_requireand that was only added inreticulate1.41, please add the version requirement toDESCRIPTION
|
@simonnwood this is now ready to merge from my perspective. |